/*
 * Copyright (c) 1997, 2011, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */
package javax.swing.text.rtf;

import java.lang.*;
import java.util.*;
import java.io.*;
import java.awt.Color;
import java.security.AccessController;
import java.security.PrivilegedAction;
import javax.swing.text.*;

/**
 * Takes a sequence of RTF tokens and text and appends the text
 * described by the RTF to a <code>StyledDocument</code> (the <em>target</em>).
 * The RTF is lexed
 * from the character stream by the <code>RTFParser</code> which is this class's
 * superclass.
 *
 * This class is an indirect subclass of OutputStream. It must be closed
 * in order to guarantee that all of the text has been sent to
 * the text acceptor.
 *
 *   @see RTFParser
 *   @see java.io.OutputStream
 */
class RTFReader extends RTFParser {
    /** The object to which the parsed text is sent. */
    StyledDocument target;

    /** Miscellaneous information about the parser's state. This
     *  dictionary is saved and restored when an RTF group begins
     *  or ends. */
    Dictionary<Object, Object> parserState; /* Current parser state */
    /** This is the "dst" item from parserState. rtfDestination
     *  is the current rtf destination. It is cached in an instance
     *  variable for speed. */
    Destination rtfDestination;
    /** This holds the current document attributes. */
    MutableAttributeSet documentAttributes;

    /** This Dictionary maps Integer font numbers to String font names. */
    Dictionary<Integer, String> fontTable;
    /** This array maps color indices to Color objects. */
    Color[] colorTable;
    /** This array maps character style numbers to Style objects. */
    Style[] characterStyles;
    /** This array maps paragraph style numbers to Style objects. */
    Style[] paragraphStyles;
    /** This array maps section style numbers to Style objects. */
    Style[] sectionStyles;

    /** This is the RTF version number, extracted from the \rtf keyword.
     *  The version information is currently not used. */
    int rtfversion;

    /** <code>true</code> to indicate that if the next keyword is unknown,
     *  the containing group should be ignored. */
    boolean ignoreGroupIfUnknownKeyword;

    /** The parameter of the most recently parsed \\ucN keyword,
    *  used for skipping alternative representations after a
    *  Unicode character. */
    int skippingCharacters;

    static private Dictionary<String, RTFAttribute> straightforwardAttributes;

    static {
        straightforwardAttributes = RTFAttributes.attributesByKeyword();
    }

    private MockAttributeSet mockery;

    /* this should be final, but there's a bug in javac... */
    /** textKeywords maps RTF keywords to single-character strings,
     *  for those keywords which simply insert some text. */
    static Dictionary<String, String> textKeywords = null;

    static {
        textKeywords = new Hashtable<String, String>();
        textKeywords.put("\\", "\\");
        textKeywords.put("{", "{");
        textKeywords.put("}", "}");
        textKeywords.put(" ", "\u00A0"); /* not in the spec... */
        textKeywords.put("~", "\u00A0"); /* nonbreaking space */
        textKeywords.put("_", "\u2011"); /* nonbreaking hyphen */
        textKeywords.put("bullet", "\u2022");
        textKeywords.put("emdash", "\u2014");
        textKeywords.put("emspace", "\u2003");
        textKeywords.put("endash", "\u2013");
        textKeywords.put("enspace", "\u2002");
        textKeywords.put("ldblquote", "\u201C");
        textKeywords.put("lquote", "\u2018");
        textKeywords.put("ltrmark", "\u200E");
        textKeywords.put("rdblquote", "\u201D");
        textKeywords.put("rquote", "\u2019");
        textKeywords.put("rtlmark", "\u200F");
        textKeywords.put("tab", "\u0009");
        textKeywords.put("zwj", "\u200D");
        textKeywords.put("zwnj", "\u200C");

        /* There is no Unicode equivalent to an optional hyphen, as far as
           I can tell. */
        textKeywords.put("-", "\u2027"); /* TODO: optional hyphen */
    }

    /* some entries in parserState */
    static final String TabAlignmentKey = "tab_alignment";
    static final String TabLeaderKey = "tab_leader";

    static Dictionary<String, char[]> characterSets;
    static boolean useNeXTForAnsi = false;

    static {
        characterSets = new Hashtable<String, char[]>();
    }

    /* TODO: per-font font encodings ( \fcharset control word ) ? */

    /**
     * Creates a new RTFReader instance. Text will be sent to
     * the specified TextAcceptor.
     *
     * @param destination The TextAcceptor which is to receive the text.
     */
    public RTFReader(StyledDocument destination) {
        int i;

        target = destination;
        parserState = new Hashtable<Object, Object>();
        fontTable = new Hashtable<Integer, String>();

        rtfversion = -1;

        mockery = new MockAttributeSet();
        documentAttributes = new SimpleAttributeSet();
    }

    /** Called when the RTFParser encounters a bin keyword in the
     *  RTF stream.
     *
     *  @see RTFParser
     */
    public void handleBinaryBlob(byte[] data) {
        if (skippingCharacters > 0) {
            /* a blob only counts as one character for skipping purposes */
            skippingCharacters--;
            return;
        }

        /* someday, someone will want to do something with blobs */
    }

    /**
     * Handles any pure text (containing no control characters) in the input
     * stream. Called by the superclass. */
    public void handleText(String text) {
        if (skippingCharacters > 0) {
            if (skippingCharacters >= text.length()) {
                skippingCharacters -= text.length();
                return;
            } else {
                text = text.substring(skippingCharacters);
                skippingCharacters = 0;
            }
        }

        if (rtfDestination != null) {
            rtfDestination.handleText(text);
            return;
        }

        warning("Text with no destination. oops.");
    }

    /** The default color for text which has no specified color. */
    Color defaultColor() {
        return Color.black;
    }

    /** Called by the superclass when a new RTF group is begun.
     *  This implementation saves the current <code>parserState</code>, and gives
     *  the current destination a chance to save its own state.
     * @see RTFParser#begingroup
     */
    public void begingroup() {
        if (skippingCharacters > 0) {
            /* TODO this indicates an error in the RTF. Log it? */
            skippingCharacters = 0;
        }

        /* we do this little dance to avoid cloning the entire state stack and
           immediately throwing it away. */
        Object oldSaveState = parserState.get("_savedState");
        if (oldSaveState != null)
            parserState.remove("_savedState");
        Dictionary<String, Object> saveState = (Dictionary<String, Object>) ((Hashtable) parserState).clone();
        if (oldSaveState != null)
            saveState.put("_savedState", oldSaveState);
        parserState.put("_savedState", saveState);

        if (rtfDestination != null)
            rtfDestination.begingroup();
    }

    /** Called by the superclass when the current RTF group is closed.
     *  This restores the parserState saved by <code>begingroup()</code>
     *  as well as invoking the endgroup method of the current
     *  destination.
     * @see RTFParser#endgroup
     */
    public void endgroup() {
        if (skippingCharacters > 0) {
            /* NB this indicates an error in the RTF. Log it? */
            skippingCharacters = 0;
        }

        Dictionary<Object, Object> restoredState = (Dictionary<Object, Object>) parserState.get("_savedState");
        Destination restoredDestination = (Destination) restoredState.get("dst");
        if (restoredDestination != rtfDestination) {
            rtfDestination.close(); /* allow the destination to clean up */
            rtfDestination = restoredDestination;
        }
        Dictionary oldParserState = parserState;
        parserState = restoredState;
        if (rtfDestination != null)
            rtfDestination.endgroup(oldParserState);
    }

    protected void setRTFDestination(Destination newDestination) {
        /* Check that setting the destination won't close the
           current destination (should never happen) */
        Dictionary previousState = (Dictionary) parserState.get("_savedState");
        if (previousState != null) {
            if (rtfDestination != previousState.get("dst")) {
                warning("Warning, RTF destination overridden, invalid RTF.");
                rtfDestination.close();
            }
        }
        rtfDestination = newDestination;
        parserState.put("dst", rtfDestination);
    }

    /** Called by the user when there is no more input (<i>i.e.</i>,
     * at the end of the RTF file.)
     *
     * @see OutputStream#close
     */
    public void close() throws IOException {
        Enumeration docProps = documentAttributes.getAttributeNames();
        while (docProps.hasMoreElements()) {
            Object propName = docProps.nextElement();
            target.putProperty(propName, documentAttributes.getAttribute(propName));
        }

        /* RTFParser should have ensured that all our groups are closed */

        warning("RTF filter done.");

        super.close();
    }

    /**
     * Handles a parameterless RTF keyword. This is called by the superclass
     * (RTFParser) when a keyword is found in the input stream.
     *
     * @returns <code>true</code> if the keyword is recognized and handled;
     *          <code>false</code> otherwise
     * @see RTFParser#handleKeyword
     */
    public boolean handleKeyword(String keyword) {
        String item;
        boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

        if (skippingCharacters > 0) {
            skippingCharacters--;
            return true;
        }

        ignoreGroupIfUnknownKeyword = false;

        if ((item = textKeywords.get(keyword)) != null) {
            handleText(item);
            return true;
        }

        if (keyword.equals("fonttbl")) {
            setRTFDestination(new FonttblDestination());
            return true;
        }

        if (keyword.equals("colortbl")) {
            setRTFDestination(new ColortblDestination());
            return true;
        }

        if (keyword.equals("stylesheet")) {
            setRTFDestination(new StylesheetDestination());
            return true;
        }

        if (keyword.equals("info")) {
            setRTFDestination(new InfoDestination());
            return false;
        }

        if (keyword.equals("mac")) {
            setCharacterSet("mac");
            return true;
        }

        if (keyword.equals("ansi")) {
            if (useNeXTForAnsi)
                setCharacterSet("NeXT");
            else
                setCharacterSet("ansi");
            return true;
        }

        if (keyword.equals("next")) {
            setCharacterSet("NeXT");
            return true;
        }

        if (keyword.equals("pc")) {
            setCharacterSet("cpg437"); /* IBM Code Page 437 */
            return true;
        }

        if (keyword.equals("pca")) {
            setCharacterSet("cpg850"); /* IBM Code Page 850 */
            return true;
        }

        if (keyword.equals("*")) {
            ignoreGroupIfUnknownKeyword = true;
            return true;
        }

        if (rtfDestination != null) {
            if (rtfDestination.handleKeyword(keyword))
                return true;
        }

        /* this point is reached only if the keyword is unrecognized */

        /* other destinations we don't understand and therefore ignore */
        if (keyword.equals("aftncn") || keyword.equals("aftnsep") || keyword.equals("aftnsepc") || keyword.equals("annotation") || keyword.equals("atnauthor") || keyword.equals("atnicn") || keyword.equals("atnid") || keyword.equals("atnref") || keyword.equals("atntime") || keyword.equals("atrfend") || keyword.equals("atrfstart") || keyword.equals("bkmkend") || keyword.equals("bkmkstart") || keyword.equals("datafield") || keyword.equals("do") || keyword.equals("dptxbxtext") || keyword.equals("falt") || keyword.equals("field") || keyword.equals("file") || keyword.equals("filetbl") || keyword.equals("fname") || keyword.equals("fontemb") || keyword.equals("fontfile") || keyword.equals("footer") || keyword.equals("footerf") || keyword.equals("footerl") || keyword.equals("footerr") || keyword.equals("footnote") || keyword.equals("ftncn") || keyword.equals("ftnsep") || keyword.equals("ftnsepc") || keyword.equals("header") || keyword.equals("headerf") || keyword.equals("headerl") || keyword.equals("headerr") || keyword.equals("keycode") || keyword.equals("nextfile") || keyword.equals("object") || keyword.equals("pict") || keyword.equals("pn") || keyword.equals("pnseclvl") || keyword.equals("pntxtb") || keyword.equals("pntxta") || keyword.equals("revtbl") || keyword.equals("rxe") || keyword.equals("tc") || keyword.equals("template") || keyword.equals("txe") || keyword.equals("xe")) {
            ignoreGroupIfUnknownKeywordSave = true;
        }

        if (ignoreGroupIfUnknownKeywordSave) {
            setRTFDestination(new DiscardingDestination());
        }

        return false;
    }

    /**
     * Handles an RTF keyword and its integer parameter.
     * This is called by the superclass
     * (RTFParser) when a keyword is found in the input stream.
     *
     * @returns <code>true</code> if the keyword is recognized and handled;
     *          <code>false</code> otherwise
     * @see RTFParser#handleKeyword
     */
    public boolean handleKeyword(String keyword, int parameter) {
        boolean ignoreGroupIfUnknownKeywordSave = ignoreGroupIfUnknownKeyword;

        if (skippingCharacters > 0) {
            skippingCharacters--;
            return true;
        }

        ignoreGroupIfUnknownKeyword = false;

        if (keyword.equals("uc")) {
            /* count of characters to skip after a unicode character */
            parserState.put("UnicodeSkip", Integer.valueOf(parameter));
            return true;
        }
        if (keyword.equals("u")) {
            if (parameter < 0)
                parameter = parameter + 65536;
            handleText((char) parameter);
            Number skip = (Number) (parserState.get("UnicodeSkip"));
            if (skip != null) {
                skippingCharacters = skip.intValue();
            } else {
                skippingCharacters = 1;
            }
            return true;
        }

        if (keyword.equals("rtf")) {
            rtfversion = parameter;
            setRTFDestination(new DocumentDestination());
            return true;
        }

        if (keyword.startsWith("NeXT") || keyword.equals("private"))
            ignoreGroupIfUnknownKeywordSave = true;

        if (rtfDestination != null) {
            if (rtfDestination.handleKeyword(keyword, parameter))
                return true;
        }

        /* this point is reached only if the keyword is unrecognized */

        if (ignoreGroupIfUnknownKeywordSave) {
            setRTFDestination(new DiscardingDestination());
        }

        return false;
    }

    private void setTargetAttribute(String name, Object value) {
        //    target.changeAttributes(new LFDictionary(LFArray.arrayWithObject(value), LFArray.arrayWithObject(name)));
    }

    /**
     * setCharacterSet sets the current translation table to correspond with
     * the named character set. The character set is loaded if necessary.
     *
     * @see AbstractFilter
     */
    public void setCharacterSet(String name) {
        Object set;

        try {
            set = getCharacterSet(name);
        } catch (Exception e) {
            warning("Exception loading RTF character set \"" + name + "\": " + e);
            set = null;
        }

        if (set != null) {
            translationTable = (char[]) set;
        } else {
            warning("Unknown RTF character set \"" + name + "\"");
            if (!name.equals("ansi")) {
                try {
                    translationTable = (char[]) getCharacterSet("ansi");
                } catch (IOException e) {
                    throw new InternalError("RTFReader: Unable to find character set resources (" + e + ")", e);
                }
            }
        }

        setTargetAttribute(Constants.RTFCharacterSet, name);
    }

    /** Adds a character set to the RTFReader's list
     *  of known character sets */
    public static void defineCharacterSet(String name, char[] table) {
        if (table.length < 256)
            throw new IllegalArgumentException("Translation table must have 256 entries.");
        characterSets.put(name, table);
    }

    /** Looks up a named character set. A character set is a 256-entry
     *  array of characters, mapping unsigned byte values to their Unicode
     *  equivalents. The character set is loaded if necessary.
     *
     *  @returns the character set
     */
    public static Object getCharacterSet(final String name) throws IOException {
        char[] set = characterSets.get(name);
        if (set == null) {
            InputStream charsetStream = AccessController.doPrivileged(new PrivilegedAction<InputStream>() {
                public InputStream run() {
                    return RTFReader.class.getResourceAsStream("charsets/" + name + ".txt");
                }
            });
            set = readCharset(charsetStream);
            defineCharacterSet(name, set);
        }
        return set;
    }

    /** Parses a character set from an InputStream. The character set
     * must contain 256 decimal integers, separated by whitespace, with
     * no punctuation. B- and C- style comments are allowed.
     *
     * @returns the newly read character set
     */
    static char[] readCharset(InputStream strm) throws IOException {
        char[] values = new char[256];
        int i;
        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(strm, "ISO-8859-1")));

        in.eolIsSignificant(false);
        in.commentChar('#');
        in.slashSlashComments(true);
        in.slashStarComments(true);

        i = 0;
        while (i < 256) {
            int ttype;
            try {
                ttype = in.nextToken();
            } catch (Exception e) {
                throw new IOException("Unable to read from character set file (" + e + ")");
            }
            if (ttype != in.TT_NUMBER) {
                //          System.out.println("Bad token: type=" + ttype + " tok=" + in.sval);
                throw new IOException("Unexpected token in character set file");
                //          continue;
            }
            values[i] = (char) (in.nval);
            i++;
        }

        return values;
    }

    static char[] readCharset(java.net.URL href) throws IOException {
        return readCharset(href.openStream());
    }

    /** An interface (could be an entirely abstract class) describing
     *  a destination. The RTF reader always has a current destination
     *  which is where text is sent.
     *
     *  @see RTFReader
     */
    interface Destination {
        void handleBinaryBlob(byte[] data);

        void handleText(String text);

        boolean handleKeyword(String keyword);

        boolean handleKeyword(String keyword, int parameter);

        void begingroup();

        void endgroup(Dictionary oldState);

        void close();
    }

    /** This data-sink class is used to implement ignored destinations
     *  (e.g. {\*\blegga blah blah blah} )
     *  It accepts all keywords and text but does nothing with them. */
    class DiscardingDestination implements Destination {
        public void handleBinaryBlob(byte[] data) {
            /* Discard binary blobs. */
        }

        public void handleText(String text) {
            /* Discard text. */
        }

        public boolean handleKeyword(String text) {
            /* Accept and discard keywords. */
            return true;
        }

        public boolean handleKeyword(String text, int parameter) {
            /* Accept and discard parameterized keywords. */
            return true;
        }

        public void begingroup() {
            /* Ignore groups --- the RTFReader will keep track of the
               current group level as necessary */
        }

        public void endgroup(Dictionary oldState) {
            /* Ignore groups */
        }

        public void close() {
            /* No end-of-destination cleanup needed */
        }
    }

    /** Reads the fonttbl group, inserting fonts into the RTFReader's
     *  fontTable dictionary. */
    class FonttblDestination implements Destination {
        int nextFontNumber;
        Integer fontNumberKey = null;
        String nextFontFamily;

        public void handleBinaryBlob(byte[] data) { /* Discard binary blobs. */
        }

        public void handleText(String text) {
            int semicolon = text.indexOf(';');
            String fontName;

            if (semicolon > -1)
                fontName = text.substring(0, semicolon);
            else
                fontName = text;

            /* TODO: do something with the font family. */

            if (nextFontNumber == -1 && fontNumberKey != null) {
                //font name might be broken across multiple calls
                fontName = fontTable.get(fontNumberKey) + fontName;
            } else {
                fontNumberKey = Integer.valueOf(nextFontNumber);
            }
            fontTable.put(fontNumberKey, fontName);

            nextFontNumber = -1;
            nextFontFamily = null;
        }

        public boolean handleKeyword(String keyword) {
            if (keyword.charAt(0) == 'f') {
                nextFontFamily = keyword.substring(1);
                return true;
            }

            return false;
        }

        public boolean handleKeyword(String keyword, int parameter) {
            if (keyword.equals("f")) {
                nextFontNumber = parameter;
                return true;
            }

            return false;
        }

        /* Groups are irrelevant. */
        public void begingroup() {
        }

        public void endgroup(Dictionary oldState) {
        }

        /* currently, the only thing we do when the font table ends is
           dump its contents to the debugging log. */
        public void close() {
            Enumeration<Integer> nums = fontTable.keys();
            warning("Done reading font table.");
            while (nums.hasMoreElements()) {
                Integer num = nums.nextElement();
                warning("Number " + num + ": " + fontTable.get(num));
            }
        }
    }

    /** Reads the colortbl group. Upon end-of-group, the RTFReader's
     *  color table is set to an array containing the read colors. */
    class ColortblDestination implements Destination {
        int red, green, blue;
        Vector<Color> proTemTable;

        public ColortblDestination() {
            red = 0;
            green = 0;
            blue = 0;
            proTemTable = new Vector<Color>();
        }

        public void handleText(String text) {
            int index;

            for (index = 0; index < text.length(); index++) {
                if (text.charAt(index) == ';') {
                    Color newColor;
                    newColor = new Color(red, green, blue);
                    proTemTable.addElement(newColor);
                }
            }
        }

        public void close() {
            int count = proTemTable.size();
            warning("Done reading color table, " + count + " entries.");
            colorTable = new Color[count];
            proTemTable.copyInto(colorTable);
        }

        public boolean handleKeyword(String keyword, int parameter) {
            if (keyword.equals("red"))
                red = parameter;
            else if (keyword.equals("green"))
                green = parameter;
            else if (keyword.equals("blue"))
                blue = parameter;
            else
                return false;

            return true;
        }

        /* Colortbls don't understand any parameterless keywords */
        public boolean handleKeyword(String keyword) {
            return false;
        }

        /* Groups are irrelevant. */
        public void begingroup() {
        }

        public void endgroup(Dictionary oldState) {
        }

        /* Shouldn't see any binary blobs ... */
        public void handleBinaryBlob(byte[] data) {
        }
    }

    /** Handles the stylesheet keyword. Styles are read and sorted
     *  into the three style arrays in the RTFReader. */
    class StylesheetDestination extends DiscardingDestination implements Destination {
        Dictionary<Integer, StyleDefiningDestination> definedStyles;

        public StylesheetDestination() {
            definedStyles = new Hashtable<Integer, StyleDefiningDestination>();
        }

        public void begingroup() {
            setRTFDestination(new StyleDefiningDestination());
        }

        public void close() {
            Vector<Style> chrStyles = new Vector<Style>();
            Vector<Style> pgfStyles = new Vector<Style>();
            Vector<Style> secStyles = new Vector<Style>();
            Enumeration<StyleDefiningDestination> styles = definedStyles.elements();
            while (styles.hasMoreElements()) {
                StyleDefiningDestination style;
                Style defined;
                style = styles.nextElement();
                defined = style.realize();
                warning("Style " + style.number + " (" + style.styleName + "): " + defined);
                String stype = (String) defined.getAttribute(Constants.StyleType);
                Vector<Style> toSet;
                if (stype.equals(Constants.STSection)) {
                    toSet = secStyles;
                } else if (stype.equals(Constants.STCharacter)) {
                    toSet = chrStyles;
                } else {
                    toSet = pgfStyles;
                }
                if (toSet.size() <= style.number)
                    toSet.setSize(style.number + 1);
                toSet.setElementAt(defined, style.number);
            }
            if (!(chrStyles.isEmpty())) {
                Style[] styleArray = new Style[chrStyles.size()];
                chrStyles.copyInto(styleArray);
                characterStyles = styleArray;
            }
            if (!(pgfStyles.isEmpty())) {
                Style[] styleArray = new Style[pgfStyles.size()];
                pgfStyles.copyInto(styleArray);
                paragraphStyles = styleArray;
            }
            if (!(secStyles.isEmpty())) {
                Style[] styleArray = new Style[secStyles.size()];
                secStyles.copyInto(styleArray);
                sectionStyles = styleArray;
            }

            /* (old debugging code)
                    int i, m;
                    if (characterStyles != null) {
                      m = characterStyles.length;
                      for(i=0;i<m;i++)
                        warnings.println("chrStyle["+i+"]="+characterStyles[i]);
                    } else warnings.println("No character styles.");
                    if (paragraphStyles != null) {
                      m = paragraphStyles.length;
                      for(i=0;i<m;i++)
                        warnings.println("pgfStyle["+i+"]="+paragraphStyles[i]);
                    } else warnings.println("No paragraph styles.");
                    if (sectionStyles != null) {
                      m = characterStyles.length;
                      for(i=0;i<m;i++)
                        warnings.println("secStyle["+i+"]="+sectionStyles[i]);
                    } else warnings.println("No section styles.");
            */
        }

        /** This subclass handles an individual style */
        class StyleDefiningDestination extends AttributeTrackingDestination implements Destination {
            final int STYLENUMBER_NONE = 222;
            boolean additive;
            boolean characterStyle;
            boolean sectionStyle;
            public String styleName;
            public int number;
            int basedOn;
            int nextStyle;
            boolean hidden;

            Style realizedStyle;

            public StyleDefiningDestination() {
                additive = false;
                characterStyle = false;
                sectionStyle = false;
                styleName = null;
                number = 0;
                basedOn = STYLENUMBER_NONE;
                nextStyle = STYLENUMBER_NONE;
                hidden = false;
            }

            public void handleText(String text) {
                if (styleName != null)
                    styleName = styleName + text;
                else
                    styleName = text;
            }

            public void close() {
                int semicolon = (styleName == null) ? 0 : styleName.indexOf(';');
                if (semicolon > 0)
                    styleName = styleName.substring(0, semicolon);
                definedStyles.put(Integer.valueOf(number), this);
                super.close();
            }

            public boolean handleKeyword(String keyword) {
                if (keyword.equals("additive")) {
                    additive = true;
                    return true;
                }
                if (keyword.equals("shidden")) {
                    hidden = true;
                    return true;
                }
                return super.handleKeyword(keyword);
            }

            public boolean handleKeyword(String keyword, int parameter) {
                if (keyword.equals("s")) {
                    characterStyle = false;
                    sectionStyle = false;
                    number = parameter;
                } else if (keyword.equals("cs")) {
                    characterStyle = true;
                    sectionStyle = false;
                    number = parameter;
                } else if (keyword.equals("ds")) {
                    characterStyle = false;
                    sectionStyle = true;
                    number = parameter;
                } else if (keyword.equals("sbasedon")) {
                    basedOn = parameter;
                } else if (keyword.equals("snext")) {
                    nextStyle = parameter;
                } else {
                    return super.handleKeyword(keyword, parameter);
                }
                return true;
            }

            public Style realize() {
                Style basis = null;
                Style next = null;

                if (realizedStyle != null)
                    return realizedStyle;

                if (basedOn != STYLENUMBER_NONE) {
                    StyleDefiningDestination styleDest;
                    styleDest = definedStyles.get(Integer.valueOf(basedOn));
                    if (styleDest != null && styleDest != this) {
                        basis = styleDest.realize();
                    }
                }

                /* NB: Swing StyleContext doesn't allow distinct styles with
                   the same name; RTF apparently does. This may confuse the
                   user. */
                realizedStyle = target.addStyle(styleName, basis);

                if (characterStyle) {
                    realizedStyle.addAttributes(currentTextAttributes());
                    realizedStyle.addAttribute(Constants.StyleType, Constants.STCharacter);
                } else if (sectionStyle) {
                    realizedStyle.addAttributes(currentSectionAttributes());
                    realizedStyle.addAttribute(Constants.StyleType, Constants.STSection);
                } else { /* must be a paragraph style */
                    realizedStyle.addAttributes(currentParagraphAttributes());
                    realizedStyle.addAttribute(Constants.StyleType, Constants.STParagraph);
                }

                if (nextStyle != STYLENUMBER_NONE) {
                    StyleDefiningDestination styleDest;
                    styleDest = definedStyles.get(Integer.valueOf(nextStyle));
                    if (styleDest != null) {
                        next = styleDest.realize();
                    }
                }

                if (next != null)
                    realizedStyle.addAttribute(Constants.StyleNext, next);
                realizedStyle.addAttribute(Constants.StyleAdditive, Boolean.valueOf(additive));
                realizedStyle.addAttribute(Constants.StyleHidden, Boolean.valueOf(hidden));

                return realizedStyle;
            }
        }
    }

    /** Handles the info group. Currently no info keywords are recognized
     *  so this is a subclass of DiscardingDestination. */
    class InfoDestination extends DiscardingDestination implements Destination {
    }

    /** RTFReader.TextHandlingDestination is an abstract RTF destination
     *  which simply tracks the attributes specified by the RTF control words
     *  in internal form and can produce acceptable AttributeSets for the
     *  current character, paragraph, and section attributes. It is up
     *  to the subclasses to determine what is done with the actual text. */
    abstract class AttributeTrackingDestination implements Destination {
        /** This is the "chr" element of parserState, cached for
         *  more efficient use */
        MutableAttributeSet characterAttributes;
        /** This is the "pgf" element of parserState, cached for
         *  more efficient use */
        MutableAttributeSet paragraphAttributes;
        /** This is the "sec" element of parserState, cached for
         *  more efficient use */
        MutableAttributeSet sectionAttributes;

        public AttributeTrackingDestination() {
            characterAttributes = rootCharacterAttributes();
            parserState.put("chr", characterAttributes);
            paragraphAttributes = rootParagraphAttributes();
            parserState.put("pgf", paragraphAttributes);
            sectionAttributes = rootSectionAttributes();
            parserState.put("sec", sectionAttributes);
        }

        abstract public void handleText(String text);

        public void handleBinaryBlob(byte[] data) {
            /* This should really be in TextHandlingDestination, but
             * since *nobody* does anything with binary blobs, this
             * is more convenient. */
            warning("Unexpected binary data in RTF file.");
        }

        public void begingroup() {
            AttributeSet characterParent = currentTextAttributes();
            AttributeSet paragraphParent = currentParagraphAttributes();
            AttributeSet sectionParent = currentSectionAttributes();

            /* It would probably be more efficient to use the
             * resolver property of the attributes set for
             * implementing rtf groups,
             * but that's needed for styles. */

            /* update the cached attribute dictionaries */
            characterAttributes = new SimpleAttributeSet();
            characterAttributes.addAttributes(characterParent);
            parserState.put("chr", characterAttributes);

            paragraphAttributes = new SimpleAttributeSet();
            paragraphAttributes.addAttributes(paragraphParent);
            parserState.put("pgf", paragraphAttributes);

            sectionAttributes = new SimpleAttributeSet();
            sectionAttributes.addAttributes(sectionParent);
            parserState.put("sec", sectionAttributes);
        }

        public void endgroup(Dictionary oldState) {
            characterAttributes = (MutableAttributeSet) parserState.get("chr");
            paragraphAttributes = (MutableAttributeSet) parserState.get("pgf");
            sectionAttributes = (MutableAttributeSet) parserState.get("sec");
        }

        public void close() {
        }

        public boolean handleKeyword(String keyword) {
            if (keyword.equals("ulnone")) {
                return handleKeyword("ul", 0);
            }

            {
                RTFAttribute attr = straightforwardAttributes.get(keyword);
                if (attr != null) {
                    boolean ok;

                    switch (attr.domain()) {
                        case RTFAttribute.D_CHARACTER:
                            ok = attr.set(characterAttributes);
                            break;
                        case RTFAttribute.D_PARAGRAPH:
                            ok = attr.set(paragraphAttributes);
                            break;
                        case RTFAttribute.D_SECTION:
                            ok = attr.set(sectionAttributes);
                            break;
                        case RTFAttribute.D_META:
                            mockery.backing = parserState;
                            ok = attr.set(mockery);
                            mockery.backing = null;
                            break;
                        case RTFAttribute.D_DOCUMENT:
                            ok = attr.set(documentAttributes);
                            break;
                        default:
                            /* should never happen */
                            ok = false;
                            break;
                    }
                    if (ok)
                        return true;
                }
            }

            if (keyword.equals("plain")) {
                resetCharacterAttributes();
                return true;
            }

            if (keyword.equals("pard")) {
                resetParagraphAttributes();
                return true;
            }

            if (keyword.equals("sectd")) {
                resetSectionAttributes();
                return true;
            }

            return false;
        }

        public boolean handleKeyword(String keyword, int parameter) {
            boolean booleanParameter = (parameter != 0);

            if (keyword.equals("fc"))
                keyword = "cf"; /* whatEVER, dude. */

            if (keyword.equals("f")) {
                parserState.put(keyword, Integer.valueOf(parameter));
                return true;
            }
            if (keyword.equals("cf")) {
                parserState.put(keyword, Integer.valueOf(parameter));
                return true;
            }

            {
                RTFAttribute attr = straightforwardAttributes.get(keyword);
                if (attr != null) {
                    boolean ok;

                    switch (attr.domain()) {
                        case RTFAttribute.D_CHARACTER:
                            ok = attr.set(characterAttributes, parameter);
                            break;
                        case RTFAttribute.D_PARAGRAPH:
                            ok = attr.set(paragraphAttributes, parameter);
                            break;
                        case RTFAttribute.D_SECTION:
                            ok = attr.set(sectionAttributes, parameter);
                            break;
                        case RTFAttribute.D_META:
                            mockery.backing = parserState;
                            ok = attr.set(mockery, parameter);
                            mockery.backing = null;
                            break;
                        case RTFAttribute.D_DOCUMENT:
                            ok = attr.set(documentAttributes, parameter);
                            break;
                        default:
                            /* should never happen */
                            ok = false;
                            break;
                    }
                    if (ok)
                        return true;
                }
            }

            if (keyword.equals("fs")) {
                StyleConstants.setFontSize(characterAttributes, (parameter / 2));
                return true;
            }

            /* TODO: superscript/subscript */

            if (keyword.equals("sl")) {
                if (parameter == 1000) { /* magic value! */
                    characterAttributes.removeAttribute(StyleConstants.LineSpacing);
                } else {
                    /* TODO: The RTF sl attribute has special meaning if it's
                       negative. Make sure that SwingText has the same special
                       meaning, or find a way to imitate that. When SwingText
                       handles this, also recognize the slmult keyword. */
                    StyleConstants.setLineSpacing(characterAttributes, parameter / 20f);
                }
                return true;
            }

            /* TODO: Other kinds of underlining */

            if (keyword.equals("tx") || keyword.equals("tb")) {
                float tabPosition = parameter / 20f;
                int tabAlignment, tabLeader;
                Number item;

                tabAlignment = TabStop.ALIGN_LEFT;
                item = (Number) (parserState.get("tab_alignment"));
                if (item != null)
                    tabAlignment = item.intValue();
                tabLeader = TabStop.LEAD_NONE;
                item = (Number) (parserState.get("tab_leader"));
                if (item != null)
                    tabLeader = item.intValue();
                if (keyword.equals("tb"))
                    tabAlignment = TabStop.ALIGN_BAR;

                parserState.remove("tab_alignment");
                parserState.remove("tab_leader");

                TabStop newStop = new TabStop(tabPosition, tabAlignment, tabLeader);
                Dictionary<Object, Object> tabs;
                Integer stopCount;

                tabs = (Dictionary<Object, Object>) parserState.get("_tabs");
                if (tabs == null) {
                    tabs = new Hashtable<Object, Object>();
                    parserState.put("_tabs", tabs);
                    stopCount = Integer.valueOf(1);
                } else {
                    stopCount = (Integer) tabs.get("stop count");
                    stopCount = Integer.valueOf(1 + stopCount.intValue());
                }
                tabs.put(stopCount, newStop);
                tabs.put("stop count", stopCount);
                parserState.remove("_tabs_immutable");

                return true;
            }

            if (keyword.equals("s") && paragraphStyles != null) {
                parserState.put("paragraphStyle", paragraphStyles[parameter]);
                return true;
            }

            if (keyword.equals("cs") && characterStyles != null) {
                parserState.put("characterStyle", characterStyles[parameter]);
                return true;
            }

            if (keyword.equals("ds") && sectionStyles != null) {
                parserState.put("sectionStyle", sectionStyles[parameter]);
                return true;
            }

            return false;
        }

        /** Returns a new MutableAttributeSet containing the
         *  default character attributes */
        protected MutableAttributeSet rootCharacterAttributes() {
            MutableAttributeSet set = new SimpleAttributeSet();

            /* TODO: default font */

            StyleConstants.setItalic(set, false);
            StyleConstants.setBold(set, false);
            StyleConstants.setUnderline(set, false);
            StyleConstants.setForeground(set, defaultColor());

            return set;
        }

        /** Returns a new MutableAttributeSet containing the
         *  default paragraph attributes */
        protected MutableAttributeSet rootParagraphAttributes() {
            MutableAttributeSet set = new SimpleAttributeSet();

            StyleConstants.setLeftIndent(set, 0f);
            StyleConstants.setRightIndent(set, 0f);
            StyleConstants.setFirstLineIndent(set, 0f);

            /* TODO: what should this be, really? */
            set.setResolveParent(target.getStyle(StyleContext.DEFAULT_STYLE));

            return set;
        }

        /** Returns a new MutableAttributeSet containing the
         *  default section attributes */
        protected MutableAttributeSet rootSectionAttributes() {
            MutableAttributeSet set = new SimpleAttributeSet();

            return set;
        }

        /**
         * Calculates the current text (character) attributes in a form suitable
         * for SwingText from the current parser state.
         *
         * @returns a new MutableAttributeSet containing the text attributes.
         */
        MutableAttributeSet currentTextAttributes() {
            MutableAttributeSet attributes = new SimpleAttributeSet(characterAttributes);
            Integer fontnum;
            Integer stateItem;

            /* figure out the font name */
            /* TODO: catch exceptions for undefined attributes,
               bad font indices, etc.? (as it stands, it is the caller's
               job to clean up after corrupt RTF) */
            fontnum = (Integer) parserState.get("f");
            /* note setFontFamily() can not handle a null font */
            String fontFamily;
            if (fontnum != null)
                fontFamily = fontTable.get(fontnum);
            else
                fontFamily = null;
            if (fontFamily != null)
                StyleConstants.setFontFamily(attributes, fontFamily);
            else
                attributes.removeAttribute(StyleConstants.FontFamily);

            if (colorTable != null) {
                stateItem = (Integer) parserState.get("cf");
                if (stateItem != null) {
                    Color fg = colorTable[stateItem.intValue()];
                    StyleConstants.setForeground(attributes, fg);
                } else {
                    /* AttributeSet dies if you set a value to null */
                    attributes.removeAttribute(StyleConstants.Foreground);
                }
            }

            if (colorTable != null) {
                stateItem = (Integer) parserState.get("cb");
                if (stateItem != null) {
                    Color bg = colorTable[stateItem.intValue()];
                    attributes.addAttribute(StyleConstants.Background, bg);
                } else {
                    /* AttributeSet dies if you set a value to null */
                    attributes.removeAttribute(StyleConstants.Background);
                }
            }

            Style characterStyle = (Style) parserState.get("characterStyle");
            if (characterStyle != null)
                attributes.setResolveParent(characterStyle);

            /* Other attributes are maintained directly in "attributes" */

            return attributes;
        }

        /**
         * Calculates the current paragraph attributes (with keys
         * as given in StyleConstants) from the current parser state.
         *
         * @returns a newly created MutableAttributeSet.
         * @see StyleConstants
         */
        MutableAttributeSet currentParagraphAttributes() {
            /* NB if there were a mutableCopy() method we should use it */
            MutableAttributeSet bld = new SimpleAttributeSet(paragraphAttributes);

            Integer stateItem;

            /*** Tab stops ***/
            TabStop tabs[];

            tabs = (TabStop[]) parserState.get("_tabs_immutable");
            if (tabs == null) {
                Dictionary workingTabs = (Dictionary) parserState.get("_tabs");
                if (workingTabs != null) {
                    int count = ((Integer) workingTabs.get("stop count")).intValue();
                    tabs = new TabStop[count];
                    for (int ix = 1; ix <= count; ix++)
                        tabs[ix - 1] = (TabStop) workingTabs.get(Integer.valueOf(ix));
                    parserState.put("_tabs_immutable", tabs);
                }
            }
            if (tabs != null)
                bld.addAttribute(Constants.Tabs, tabs);

            Style paragraphStyle = (Style) parserState.get("paragraphStyle");
            if (paragraphStyle != null)
                bld.setResolveParent(paragraphStyle);

            return bld;
        }

        /**
         * Calculates the current section attributes
         * from the current parser state.
         *
         * @returns a newly created MutableAttributeSet.
         */
        public AttributeSet currentSectionAttributes() {
            MutableAttributeSet attributes = new SimpleAttributeSet(sectionAttributes);

            Style sectionStyle = (Style) parserState.get("sectionStyle");
            if (sectionStyle != null)
                attributes.setResolveParent(sectionStyle);

            return attributes;
        }

        /** Resets the filter's internal notion of the current character
         *  attributes to their default values. Invoked to handle the
         *  \plain keyword. */
        protected void resetCharacterAttributes() {
            handleKeyword("f", 0);
            handleKeyword("cf", 0);

            handleKeyword("fs", 24); /* 12 pt. */

            Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
            while (attributes.hasMoreElements()) {
                RTFAttribute attr = attributes.nextElement();
                if (attr.domain() == RTFAttribute.D_CHARACTER)
                    attr.setDefault(characterAttributes);
            }

            handleKeyword("sl", 1000);

            parserState.remove("characterStyle");
        }

        /** Resets the filter's internal notion of the current paragraph's
         *  attributes to their default values. Invoked to handle the
         *  \pard keyword. */
        protected void resetParagraphAttributes() {
            parserState.remove("_tabs");
            parserState.remove("_tabs_immutable");
            parserState.remove("paragraphStyle");

            StyleConstants.setAlignment(paragraphAttributes, StyleConstants.ALIGN_LEFT);

            Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
            while (attributes.hasMoreElements()) {
                RTFAttribute attr = attributes.nextElement();
                if (attr.domain() == RTFAttribute.D_PARAGRAPH)
                    attr.setDefault(characterAttributes);
            }
        }

        /** Resets the filter's internal notion of the current section's
         *  attributes to their default values. Invoked to handle the
         *  \sectd keyword. */
        protected void resetSectionAttributes() {
            Enumeration<RTFAttribute> attributes = straightforwardAttributes.elements();
            while (attributes.hasMoreElements()) {
                RTFAttribute attr = attributes.nextElement();
                if (attr.domain() == RTFAttribute.D_SECTION)
                    attr.setDefault(characterAttributes);
            }

            parserState.remove("sectionStyle");
        }
    }

    /** RTFReader.TextHandlingDestination provides basic text handling
     *  functionality. Subclasses must implement: <dl>
     *  <dt>deliverText()<dd>to handle a run of text with the same
     *                       attributes
     *  <dt>finishParagraph()<dd>to end the current paragraph and
     *                           set the paragraph's attributes
     *  <dt>endSection()<dd>to end the current section
     *  </dl>
     */
    abstract class TextHandlingDestination extends AttributeTrackingDestination implements Destination {
        /** <code>true</code> if the reader has not just finished
         *  a paragraph; false upon startup */
        boolean inParagraph;

        public TextHandlingDestination() {
            super();
            inParagraph = false;
        }

        public void handleText(String text) {
            if (!inParagraph)
                beginParagraph();

            deliverText(text, currentTextAttributes());
        }

        abstract void deliverText(String text, AttributeSet characterAttributes);

        public void close() {
            if (inParagraph)
                endParagraph();

            super.close();
        }

        public boolean handleKeyword(String keyword) {
            if (keyword.equals("\r") || keyword.equals("\n")) {
                keyword = "par";
            }

            if (keyword.equals("par")) {
                //          warnings.println("Ending paragraph.");
                endParagraph();
                return true;
            }

            if (keyword.equals("sect")) {
                //          warnings.println("Ending section.");
                endSection();
                return true;
            }

            return super.handleKeyword(keyword);
        }

        protected void beginParagraph() {
            inParagraph = true;
        }

        protected void endParagraph() {
            AttributeSet pgfAttributes = currentParagraphAttributes();
            AttributeSet chrAttributes = currentTextAttributes();
            finishParagraph(pgfAttributes, chrAttributes);
            inParagraph = false;
        }

        abstract void finishParagraph(AttributeSet pgfA, AttributeSet chrA);

        abstract void endSection();
    }

    /** RTFReader.DocumentDestination is a concrete subclass of
     *  TextHandlingDestination which appends the text to the
     *  StyledDocument given by the <code>target</code> ivar of the
     *  containing RTFReader.
     */
    class DocumentDestination extends TextHandlingDestination implements Destination {
        public void deliverText(String text, AttributeSet characterAttributes) {
            try {
                target.insertString(target.getLength(), text, currentTextAttributes());
            } catch (BadLocationException ble) {
                /* This shouldn't be able to happen, of course */
                /* TODO is InternalError the correct error to throw? */
                throw new InternalError(ble.getMessage(), ble);
            }
        }

        public void finishParagraph(AttributeSet pgfAttributes, AttributeSet chrAttributes) {
            int pgfEndPosition = target.getLength();
            try {
                target.insertString(pgfEndPosition, "\n", chrAttributes);
                target.setParagraphAttributes(pgfEndPosition, 1, pgfAttributes, true);
            } catch (BadLocationException ble) {
                /* This shouldn't be able to happen, of course */
                /* TODO is InternalError the correct error to throw? */
                throw new InternalError(ble.getMessage(), ble);
            }
        }

        public void endSection() {
            /* If we implemented sections, we'd end 'em here */
        }
    }

}
